A website for the ATmosphereConf
1---
2import '../../styles.css'
3import { getSession } from '../../lib/session'
4import { getOAuthClient } from '../../lib/context'
5import { Agent } from '@atproto/api'
6
7const { handle } = Astro.params
8
9if (!handle) {
10 return Astro.redirect('/')
11}
12
13const session = getSession(Astro.cookies)
14const oauthClient = getOAuthClient(Astro.cookies)
15
16let agent: Agent | null = null
17let profile: any = null
18let conferenceProfile: any = null
19let did: string | null = null
20let isOwnProfile = false
21
22// Get agent if authenticated
23if (session.did) {
24 try {
25 const oauthSession = await oauthClient.restore(session.did)
26 if (oauthSession) {
27 agent = new Agent(oauthSession)
28 isOwnProfile = agent.assertDid === session.did
29 }
30 } catch (err) {
31 console.warn('OAuth restore failed:', err)
32 }
33}
34
35// Create a public agent to resolve the profile if we don't have an authenticated one
36const publicAgent = agent || new Agent({ service: 'https://public.api.bsky.app' })
37
38// Resolve handle to DID and get profile
39try {
40 const resolveResponse = await publicAgent.resolveHandle({ handle })
41 did = resolveResponse.data.did
42
43 // Get Bluesky profile for basic info
44 try {
45 const profileResponse = await publicAgent.app.bsky.actor.getProfile({
46 actor: did,
47 })
48 profile = profileResponse.data
49 } catch (err) {
50 console.warn('Failed to fetch Bluesky profile:', err)
51 }
52
53 // Get conference profile
54 try {
55 const response = await publicAgent.com.atproto.repo.getRecord({
56 repo: did,
57 collection: 'org.atmosphereconf.profile',
58 rkey: 'self'
59 })
60 conferenceProfile = response.data.value
61 } catch (err) {
62 console.log('No conference profile found for this user')
63 }
64} catch (err) {
65 console.error('Failed to resolve handle:', err)
66 return new Response('Profile not found', { status: 404 })
67}
68
69// Helper function to convert blob refs to URLs
70function blobRefToUrl(blobRef: any, did: string): string {
71 if (!blobRef || typeof blobRef !== 'object') return ''
72
73 // Handle BlobRef object with CID
74 if (blobRef.ref) {
75 const cid = blobRef.ref.toString()
76 return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}@jpeg`
77 }
78
79 return ''
80}
81
82const displayName = conferenceProfile?.displayName || profile?.displayName || handle
83const description = conferenceProfile?.description || profile?.description || ''
84
85// Handle both blob refs and direct URLs
86let avatar = ''
87if (conferenceProfile?.avatar) {
88 avatar = blobRefToUrl(conferenceProfile.avatar, did)
89} else if (profile?.avatar) {
90 avatar = profile.avatar
91}
92
93let banner = ''
94if (conferenceProfile?.banner) {
95 banner = blobRefToUrl(conferenceProfile.banner, did)
96} else if (profile?.banner) {
97 banner = profile.banner
98}
99
100const hasConferenceProfile = !!conferenceProfile
101---
102
103<html lang="en" data-theme="dracula">
104 <head>
105 <meta charset="utf-8" />
106 <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
107 <meta name="viewport" content="width=device-width" />
108 <meta name="generator" content={Astro.generator} />
109 <title>{displayName} - ATmosphere</title>
110 </head>
111 <body>
112 <div class="min-h-screen bg-base-300">
113 {banner && (
114 <div class="w-full h-48 md:h-64 bg-base-200">
115 <img
116 src={banner}
117 alt="Profile banner"
118 class="w-full h-full object-cover"
119 />
120 </div>
121 )}
122
123 <div class="container mx-auto px-4 -mt-16 relative z-10 max-w-4xl">
124 <div class="card bg-base-200 shadow-xl">
125 <div class="card-body">
126 <div class="flex flex-col md:flex-row gap-6">
127 <div class="flex-shrink-0">
128 {avatar ? (
129 <div class="avatar">
130 <div class="w-32 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2">
131 <img src={avatar} alt={displayName} />
132 </div>
133 </div>
134 ) : (
135 <div class="avatar placeholder">
136 <div class="bg-neutral text-neutral-content rounded-full w-32">
137 <span class="text-3xl">{displayName[0]?.toUpperCase()}</span>
138 </div>
139 </div>
140 )}
141 </div>
142
143 <div class="flex-grow">
144 <div class="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
145 <div>
146 <h1 class="text-3xl font-bold">{displayName}</h1>
147 <p class="text-sm opacity-70">@{handle}</p>
148 {did && (
149 <p class="text-xs opacity-50 mt-1 font-mono break-all">
150 {did}
151 </p>
152 )}
153 </div>
154
155 {isOwnProfile && (
156 <a href="/profile/create" class="btn btn-primary btn-sm">
157 Edit Profile
158 </a>
159 )}
160 </div>
161
162 {description && (
163 <p class="mt-4 text-base whitespace-pre-wrap">{description}</p>
164 )}
165
166 <div class="mt-4">
167 {hasConferenceProfile ? (
168 <div class="badge badge-success gap-2">
169 <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-4 h-4 stroke-current">
170 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
171 </svg>
172 Conference Attendee
173 </div>
174 ) : (
175 <div class="badge badge-ghost gap-2">
176 No Conference Profile
177 </div>
178 )}
179 </div>
180 </div>
181 </div>
182
183 {!hasConferenceProfile && isOwnProfile && (
184 <div class="alert alert-warning mt-6">
185 <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
186 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
187 </svg>
188 <span>You haven't set up your conference profile yet.</span>
189 <div>
190 <a href="/profile/create" class="btn btn-sm btn-primary">Create Now</a>
191 </div>
192 </div>
193 )}
194 </div>
195 </div>
196
197 <div class="mt-6 mb-12">
198 <a href="/" class="btn btn-ghost">
199 <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
200 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
201 </svg>
202 Back to Home
203 </a>
204 </div>
205 </div>
206 </div>
207 </body>
208</html>